package blazingbubble.legacy.game;

import spark.math.Vector3;
import spark.utility.Clock;
import spark.math.Random;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Vector;
import java.util.Map.Entry;

import blazingbubble.legacy.Point;
import blazingbubble.legacy.Pair;

public class BubbleField implements Iterable<Bubble>
{
	public class ContainedBubbleIterator implements Iterator<Bubble>

	{
		private Vector<Iterator<Bubble>> iterators = new Vector<Iterator<Bubble>>();
		private Iterator<Iterator<Bubble>> iteratorsIterator;
		private Iterator<Bubble> currentIterator;

		public ContainedBubbleIterator()
		{
			iterators.add(attachedBubbles.iterator());
			iterators.add(movingBubbles.iterator());
			iterators.add(fallingBubbles.iterator());
			if(chainReactions)
				iterators.add(chainReactingBubbles.iterator());
			iteratorsIterator = iterators.iterator();
			if(iteratorsIterator.hasNext())
				currentIterator = iteratorsIterator.next();
			else
				currentIterator = new Vector<Bubble>().iterator();
		}
		public boolean hasNext()
		{
			if(currentIterator.hasNext())
				return true;
			if(iteratorsIterator.hasNext())
			{
				currentIterator = iteratorsIterator.next();
				return hasNext();
			}
			return false;
		}

		public Bubble next()
		{
			return currentIterator.next();
		}

		public void remove()
		{
			currentIterator.remove();
		}
	}

	//IMPLEMENTATIONS
	private Vector<Bubble> chainReactingBubbles = null;
	private HashMap<Bubble.Type, Vector<Bubble>> chainReactionCandidates = null;
	public LinkedList<ChainReaction> activeChainReactions = null;
	private LinkedList<Bubble> toBePopped = null;
	//IMPLEMENTATIONS END

	public Vector<Bubble> debugBubbles = new Vector<Bubble>();

	private int criticalBubbleMass = 3;
	private int widthInBubbles;
	private int heightInBubbles;
	public static float width;
	public static float height;
	private static final float fallingBubbleKillY = -5;
	private Vector3 gravity = new Vector3(0, -10, 0);
	private Vector3 fallingBubbleForce = new Vector3(0, -36, 4);
	private Vector<Bubble> attachedBubbles = new Vector<Bubble>();
	private HashMap<Bubble.Type, Vector<Bubble>> attachedBubblesByType = new HashMap<Bubble.Type, Vector<Bubble>>();
	private Vector<Bubble> movingBubbles = new Vector<Bubble>();
	private Vector<Bubble> fallingBubbles = new Vector<Bubble>();
	private BubbleGrid grid;

	private boolean doesGravityAffectMovingBubbles = false;
	private boolean chainReactions = true;

	public BubbleField(int widthInBubbles, int heightInBubbles)
	{
		if(chainReactions)
		{
			chainReactingBubbles = new Vector<Bubble>();
			chainReactionCandidates = new HashMap<Bubble.Type, Vector<Bubble>>();
			activeChainReactions = new LinkedList<ChainReaction>();
			toBePopped = new LinkedList<Bubble>();
		}
		this.widthInBubbles = widthInBubbles;
		this.heightInBubbles = heightInBubbles;
		width = (float)widthInBubbles * 2f * Bubble.getRadius();
		height = 2f * Bubble.getRadius() + ((heightInBubbles-1) * 2f * Bubble.getRadius()) * 0.875f;
		grid = new BubbleGrid(heightInBubbles, widthInBubbles, Bubble.getRadius());
		for(Iterator<Bubble.Type> i = Bubble.Type.iterator(); i.hasNext();)
		{
			attachedBubblesByType.put(i.next(), new Vector<Bubble>());
		}
	}

	public void addDebugBubble(Bubble bubble)
	{
		bubble.getPosition().z = 0.1f;
		debugBubbles.add(bubble);
	}

	private boolean collidesLeftRight(Bubble bubble, Vector3 movement)
	{
		return ( bubble.getPosition().x + movement.x - bubble.getCollisionRadius() < 0  ||
				bubble.getPosition().x + movement.x + bubble.getCollisionRadius() > width);
	}

	private float moveUntilCollisionPointLeftRight(Bubble bubble, int accuracy)
	{
		Vector3 movementThisFrame = getBubbleMovement(bubble, Clock.getInstance().getElapsedSeconds());
		float timeUntilCollision = Clock.getInstance().getElapsedSeconds();
		Vector3 changeInMovement = Vector3.multiply(movementThisFrame, 0.5f);
		float changeInTime = timeUntilCollision * 0.5f;
		for(int i = 0; i < accuracy; i++)
		{
			do
			{
				movementThisFrame.substract(changeInMovement);
				timeUntilCollision -= changeInTime;
			} while(collidesLeftRight(bubble, movementThisFrame));
			changeInMovement.multiply(0.5f);
			changeInTime *= 0.5f;
			do
			{
				movementThisFrame.add(changeInMovement);
				timeUntilCollision += changeInTime;
			} while(!collidesLeftRight(bubble, movementThisFrame));
			changeInMovement.multiply(0.5f);
			changeInTime *= 0.5f;
		}
		bubble.getPosition().add(movementThisFrame);
		return timeUntilCollision;
	}

	private boolean collidesTop(Bubble bubble, Vector3 movement)
	{
		return ( bubble.getPosition().y + movement.y + bubble.getCollisionRadius() > height);
	}

	private float moveUntilCollisionPointTop(Bubble bubble, int accuracy)
	{
		Vector3 movementThisFrame = getBubbleMovement(bubble, Clock.getInstance().getElapsedSeconds());
		float timeUntilCollision = Clock.getInstance().getElapsedSeconds();
		Vector3 changeInMovement = Vector3.multiply(movementThisFrame, 0.5f);
		float changeInTime = timeUntilCollision * 0.5f;
		for(int i = 0; i < accuracy; i++)
		{
			do
			{
				movementThisFrame.substract(changeInMovement);
				timeUntilCollision -= changeInTime;
			} while(collidesTop(bubble, movementThisFrame));
			changeInMovement.multiply(0.5f);
			changeInTime *= 0.5f;
			do
			{
				movementThisFrame.add(changeInMovement);
				timeUntilCollision += changeInTime;
			} while(!collidesTop(bubble, movementThisFrame));
			changeInMovement.multiply(0.5f);
			changeInTime *= 0.5f;
		}
		bubble.getPosition().add(movementThisFrame);
		return timeUntilCollision;
	}

	private Vector3 getBubbleMovement(Bubble bubble, float secondsToMove)
	{
		if(doesGravityAffectMovingBubbles)
		{
			bubble.getVelocity().add(Vector3.multiply(gravity, secondsToMove));
		}
		Vector3 movement = Vector3.multiply(bubble.getVelocity(), secondsToMove);
		return movement;
	}

	public void update()
	{
		if(chainReactions)
			updateChainReactingBubbles();
		updateMovingBubbles();
		updateFallingBubbles();
	}

	private Vector<Pair<Point, LinkedList<Bubble>>> fetchPossibleChainReactionPoints(Bubble.Type type)
	{
		Vector<Pair<Point, LinkedList<Bubble>>> possibleAttachingPositions = new Vector<Pair<Point, LinkedList<Bubble>>>();
		if(!type.isColored())
			return possibleAttachingPositions;
        for(Bubble bubble1 : attachedBubblesByType.get(type))
        {
            gatherAttachingPoints(bubble1, possibleAttachingPositions);
        }
        for(Bubble bubble : attachedBubblesByType.get(Bubble.Type.Rainbow))
        {
            gatherAttachingPoints(bubble, possibleAttachingPositions);
        }

		Collections.sort(possibleAttachingPositions, new AttachingPointComparator());

		return possibleAttachingPositions;
	}

	private void gatherAttachingPoints(Bubble centerBubble, Vector<Pair<Point, LinkedList<Bubble>>> possibleAttachingPositions)
	{
		Bubble temporaryBubble = new Bubble(new Vector3(), null, centerBubble.getType());
		Vector<Point> emptyNeighbourCells = grid.getEmptyNeighbourCells(centerBubble.getGridPositionX(), centerBubble.getGridPositionY());
        for(Point c : emptyNeighbourCells)
        {
            LinkedList<Bubble> theoreticalPopBubbles = getBubbleMassOnPop(temporaryBubble, c.x, c.y);
            if(theoreticalPopBubbles.size() == 0)
            {
                continue;
            }
            possibleAttachingPositions.add(new Pair<Point, LinkedList<Bubble>>(c, theoreticalPopBubbles));
        }
	}

	private void updateChainReactingBubbles()
	{
		if(activeChainReactions.size() > 0)
		{
			System.out.println("Active : " + activeChainReactions.size());
		}
		LinkedList<Bubble> bubblesInPlace = new LinkedList<Bubble>();
		for(Iterator<ChainReaction> i = activeChainReactions.iterator(); i.hasNext();)
		{
			ChainReaction current = i.next();

			current.update();
			if(current.isDone())
			{
				chainReactingBubbles.remove(current.getBubble());
				current.getBubble().setPosition(current.getDestination());
				bubblesInPlace.add(current.getBubble());
				i.remove();
			}
		}
        for(Bubble aBubblesInPlace : bubblesInPlace)
        {
            attachBubble(aBubblesInPlace);
        }
	}

	private void updateFallingBubbles()
	{
		for(Iterator<Bubble> i = fallingBubbles.iterator(); i.hasNext();)
		{
			Bubble currentBubble = i.next();
			currentBubble.getPosition().add(Vector3.multiply(currentBubble.getVelocity(), Clock.getInstance().getElapsedSeconds()));
			currentBubble.getVelocity().add(Vector3.multiply(fallingBubbleForce, Clock.getInstance().getElapsedSeconds()));
			if(currentBubble.getPosition().y < fallingBubbleKillY)
			{
				i.remove();
			}
		}
	}

	private void updateMovingBubbles()
	{
		for(Iterator<Bubble> movingBubbleIterator = movingBubbles.iterator(); movingBubbleIterator.hasNext();)
		{
			Bubble currentMovingBubble = movingBubbleIterator.next();
			Vector3 movementThisFrame = getBubbleMovement(currentMovingBubble, Clock.getInstance().getElapsedSeconds());

			//collision against gamefield bounds
			if(collidesLeftRight(currentMovingBubble, movementThisFrame))
			{
				float timeUntilCollision = moveUntilCollisionPointLeftRight(currentMovingBubble, 10);
				currentMovingBubble.getVelocity().x *= -1;
				movementThisFrame = getBubbleMovement(currentMovingBubble, Clock.getInstance().getElapsedSeconds() - timeUntilCollision);
			}
			if(collidesTop(currentMovingBubble, movementThisFrame))
			{
				moveUntilCollisionPointTop(currentMovingBubble, 5);
				movingBubbleIterator.remove();
				attachBubble(currentMovingBubble);
				continue;
			}


			//collision against attatched bubbles
			float timeUntilCollision = Float.MAX_VALUE;
			Bubble collisionPartner = null;
			//iterate over the attached bubbles and see if the moving bubble will collide with it
			//if it collides with multiple attached bubbles find the first collision partner
            for(Bubble currentAttachedBubble : attachedBubbles)
            {
                if(currentMovingBubble.willCollide(currentAttachedBubble))
                {
                    float currentTimeUntilCollision = currentMovingBubble.getCollisionTime(currentAttachedBubble);
                    if(currentTimeUntilCollision < timeUntilCollision)
                    {
                        timeUntilCollision = currentTimeUntilCollision;
                        collisionPartner = currentAttachedBubble;
                    }
                }
            }
			if(collisionPartner == null)
			{
				currentMovingBubble.getPosition().add(movementThisFrame);
			}
			else
			{
				currentMovingBubble.getPosition().add(
						getBubbleMovement(currentMovingBubble, timeUntilCollision));
				movingBubbleIterator.remove();
				attachBubble(currentMovingBubble);
			}
		}
	}

	public void positionBubble(Bubble bubble)
	{
		attachedBubbles.add(bubble);
		attachedBubblesByType.get(bubble.getType()).add(bubble);
		bubble.setVelocity(new Vector3(0,0,0));
		grid.snapToGrid(bubble);
	}

	private void attachBubble(Bubble bubble)
	{
		attachedBubbles.add(bubble);
		attachedBubblesByType.get(bubble.getType()).add(bubble);
		bubble.setVelocity(new Vector3(0,0,0));
		grid.snapToGrid(bubble);
		if(bubble.getType().isColored() || bubble.getType() == Bubble.Type.Rainbow)
		{
			LinkedList<Bubble> bubbleChain = bubble.getBubbleChain().getChainBubblesByCriticalMass(criticalBubbleMass);
			if(bubbleChain.isEmpty())
				return;
			dissolveBubbles(bubbleChain, true);
		}
		else if(bubble.getType() == Bubble.Type.Blazing)
		{
			Vector<Bubble> toDestroy = new Vector<Bubble>(bubble.getAttached());
			toDestroy.add(bubble);
			dissolveBubbles(toDestroy, false);
		}
	}

	private void dissolveBubbles(Collection<Bubble> bubbles, boolean dissolveSurroundingGhostBubbles)
	{
		Vector<Bubble> chain = new Vector<Bubble>(bubbles);
		Vector<Bubble> surroundingBubbles = Bubble.getSurroundingBubbles(bubbles);
        for(Bubble currentBubble : chain)
        {
            popBubble(currentBubble);
        }

		if(chainReactions)
		{
			chainReactionCandidates.clear();
		}

        for(Bubble currentBubble : surroundingBubbles)
        {
            if(currentBubble.isHangingFree())
            {
                popBubbleMass(currentBubble);
                continue;
            }
            if(dissolveSurroundingGhostBubbles && currentBubble.getType() == Bubble.Type.Ghost)
            {
                Vector<Bubble> bubblesAroundGhostBubble = new Vector<Bubble>(currentBubble.getAttached());
                popBubble(currentBubble);
                for(Bubble currentBubbleAroundGhostBubble : bubblesAroundGhostBubble)
                {
                    if(currentBubbleAroundGhostBubble.isHangingFree())
                    {
                        popBubbleMass(currentBubbleAroundGhostBubble);
                        continue;
                    }
                }
            }
        }
		if(chainReactions)
		{
			initializeChainReactions(chainReactionCandidates);
		}
	}

	private void popBubbleMass(Bubble bubble)
	{
        for(Bubble currentBubble : bubble.gatherBubbleMass())
        {
            popBubble(currentBubble);
            if(chainReactions)
            {
                if(!chainReactionCandidates.containsKey(currentBubble.getType()))
                {
                    chainReactionCandidates.put(currentBubble.getType(), new Vector<Bubble>());
                }
                chainReactionCandidates.get(currentBubble.getType()).add(currentBubble);
            }
        }
	}

	private void initializeChainReactions(HashMap<Bubble.Type, Vector<Bubble>> bubbles)
	{
        for(Entry<Bubble.Type, Vector<Bubble>> currentEntry : bubbles.entrySet())
        {
            if(!currentEntry.getKey().isColored())
            {
                continue;
            }
            Vector<Pair<Point, LinkedList<Bubble>>> attachingPoints = fetchPossibleChainReactionPoints(currentEntry.getKey());
            Iterator<Bubble> chainReactBubblesIterator = currentEntry.getValue().iterator();
            Iterator<Pair<Point, LinkedList<Bubble>>> chainReactPointsIterator = attachingPoints.iterator();
            while(chainReactBubblesIterator.hasNext() && chainReactPointsIterator.hasNext())
            {
                Pair<Point, LinkedList<Bubble>> currentPoint = chainReactPointsIterator.next();
                if(toBePopped.containsAll(currentPoint.getSecond()))
                {
                    continue;
                }
                toBePopped.addAll(currentPoint.getSecond());
                Bubble currentBubble = chainReactBubblesIterator.next();
                fallingBubbles.remove(currentBubble);
                chainReactingBubbles.add(currentBubble);
                activeChainReactions.add(new ChainReaction(currentBubble, grid.getWorldCoordinates(currentPoint.getFirst().x, currentPoint.getFirst().y)));
            }
        }
	}

	private void popBubble(Bubble bubble)
	{
		grid.resetField(bubble.getGridPositionX(), bubble.getGridPositionY());
		initializePoppingVelocity(bubble);
		bubble.disattach();
		for(Iterator<Bubble> i = toBePopped.iterator(); i.hasNext();)
		{
			if(i.next() == bubble)
				i.remove();
		}
		attachedBubbles.remove(bubble);
		attachedBubblesByType.get(bubble.getType()).remove(bubble);
		fallingBubbles.add(bubble);
	}

	private void initializePoppingVelocity(Bubble bubble)
	{
		bubble.getVelocity().x = bubble.getPosition().x - width / 2;
		//bubble.getVelocity().x = Random.getInstance().getRandom().nextFloat() * width / 2;
		bubble.getVelocity().y = -gravity.y * (4 + Random.getInstance().nextInt(3)) * 0.1f;
	}

	private LinkedList<Bubble> getBubbleMassOnPop(Bubble bubble, int gridPositionX, int gridPositionY)
	{
		grid.snapToGrid(bubble, gridPositionX, gridPositionY);
		LinkedList<Bubble> poppedBubbles = bubble.getBubbleChain().getChainBubblesByCriticalMass(criticalBubbleMass);
		poppedBubbles.remove(bubble);
		grid.resetField(bubble.getGridPositionX(), bubble.getGridPositionY());
		bubble.disattach();
		return poppedBubbles;
	}

	public Iterator<Bubble> iterator()
	{
		return new ContainedBubbleIterator();
	}

	public void spawnMovingBubble(Bubble bubble)
	{
		movingBubbles.add(bubble);
	}

	public float getWidth() { return width; }
	public float getHeight() { return height; }
	public int getWidthInBubbles() { return widthInBubbles; }
	public int getHeightInBubbles() { return heightInBubbles; }
	public Vector3 getFallingBubbleForce() { return fallingBubbleForce; }
	public void setFallingBubbleForce(Vector3 fallingBubbleForce) { this.fallingBubbleForce = fallingBubbleForce; }
	public Vector3 getGravity() { return gravity; }
	public void setGravity(Vector3 gravity) { this.gravity = gravity; }

}
